<!DOCTYPE HTML>
<html lang="zh" class="sidebar-visible no-js light">
    <head>
        <!-- Book generated using https://github.com/wa-lang/wabook -->
        <meta charset="UTF-8">
        <title>和数据库打交道 - Go语言高级编程</title>
        <!-- Custom HTML head -->
        <meta content="text/html; charset=utf-8" http-equiv="Content-Type">
        <meta name="description" content="">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta name="theme-color" content="#ffffff" />

        <link rel="icon" href="../favicon.svg">
        <link rel="shortcut icon" href="../favicon.png">
        <link rel="stylesheet" href="../static/wabook/css/variables.css">
        <link rel="stylesheet" href="../static/wabook/css/general.css">
        <link rel="stylesheet" href="../static/wabook/css/chrome.css">
        <link rel="stylesheet" href="../static/wabook/css/print.css" media="print">
        <!-- Fonts -->
        <link rel="stylesheet" href="../static/wabook/FontAwesome/css/font-awesome.css">
        <link rel="stylesheet" href="../static/wabook/fonts/fonts.css">
        <!-- Highlight.js Stylesheets -->
        <link rel="stylesheet" href="../static/wabook/highlight.css">
        <link rel="stylesheet" href="../static/wabook/tomorrow-night.css">
        <link rel="stylesheet" href="../static/wabook/ayu-highlight.css">

        <!-- Custom theme stylesheets -->
    </head>
    <body>
        <!-- Provide site root to javascript -->
        <script type="text/javascript">
            var path_to_root = "../";
            var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "light";
        </script>

        <!-- Work around some values being stored in localStorage wrapped in quotes -->
        <script type="text/javascript">
            try {
                var theme = localStorage.getItem('wabook-theme');
                var sidebar = localStorage.getItem('wabook-sidebar');

                if (theme.startsWith('"') && theme.endsWith('"')) {
                    localStorage.setItem('wabook-theme', theme.slice(1, theme.length - 1));
                }

                if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
                    localStorage.setItem('wabook-sidebar', sidebar.slice(1, sidebar.length - 1));
                }
            } catch (e) { }
        </script>

        <!-- Set the theme before any content is loaded, prevents flash -->
        <script type="text/javascript">
            var theme;
            try { theme = localStorage.getItem('wabook-theme'); } catch(e) { }
            if (theme === null || theme === undefined) { theme = default_theme; }
            var html = document.querySelector('html');
            html.classList.remove('no-js')
            html.classList.remove('light')
            html.classList.add(theme);
            html.classList.add('js');
        </script>

        <!-- Hide / unhide sidebar before it is displayed -->
        <script type="text/javascript">
            var html = document.querySelector('html');
            var sidebar = 'hidden';
            if (document.body.clientWidth >= 1080) {
                try { sidebar = localStorage.getItem('wabook-sidebar'); } catch(e) { }
                sidebar = sidebar || 'visible';
            }
            html.classList.remove('sidebar-visible');
            html.classList.add("sidebar-" + sidebar);
        </script>

        <nav id="sidebar" class="sidebar" aria-label="Table of contents">
            <div class="sidebar-scrollbox">
                <ol class="chapter">
  <li class="chapter-item expanded ">
    <a href="../index.html" >Go语言高级编程</a>
  </li>
  <li class="chapter-item expanded ">
    <a href="../preface.html" >前言</a>
  </li>
  <li class="chapter-item expanded ">
    <a href="../ch1-basic\readme.html" ><strong aria-hidden="true">1.</strong> 语言基础</a>
  </li>
  <ol class="section">
    <li class="chapter-item expanded ">
      <a href="../ch1-basic\ch1-01-genesis.html" ><strong aria-hidden="true">1.1.</strong> Go语言创世纪</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../ch1-basic\ch1-02-hello-revolution.html" ><strong aria-hidden="true">1.2.</strong> Hello, World 的革命</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../ch1-basic\ch1-03-array-string-and-slice.html" ><strong aria-hidden="true">1.3.</strong> 数组、字符串和切片</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../ch1-basic\ch1-04-func-method-interface.html" ><strong aria-hidden="true">1.4.</strong> 函数、方法和接口</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../ch1-basic\ch1-05-mem.html" ><strong aria-hidden="true">1.5.</strong> 面向并发的内存模型</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../ch1-basic\ch1-06-goroutine.html" ><strong aria-hidden="true">1.6.</strong> 常见的并发模式</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../ch1-basic\ch1-07-error-and-panic.html" ><strong aria-hidden="true">1.7.</strong> 错误和异常</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../ch1-basic\ch1-08-ext.html" ><strong aria-hidden="true">1.8.</strong> 补充说明</a>
    </li>
  </ol>
  <li class="chapter-item expanded ">
    <a href="../ch2-cgo\readme.html" ><strong aria-hidden="true">2.</strong> CGO编程</a>
  </li>
  <ol class="section">
    <li class="chapter-item expanded ">
      <a href="../ch2-cgo\ch2-01-hello-cgo.html" ><strong aria-hidden="true">2.1.</strong> 快速入门</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../ch2-cgo\ch2-02-basic.html" ><strong aria-hidden="true">2.2.</strong> CGO基础</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../ch2-cgo\ch2-03-cgo-types.html" ><strong aria-hidden="true">2.3.</strong> 类型转换</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../ch2-cgo\ch2-04-func.html" ><strong aria-hidden="true">2.4.</strong> 函数调用</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../ch2-cgo\ch2-05-internal.html" ><strong aria-hidden="true">2.5.</strong> 内部机制</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../ch2-cgo\ch2-06-qsort.html" ><strong aria-hidden="true">2.6.</strong> 实战: 封装qsort</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../ch2-cgo\ch2-07-memory.html" ><strong aria-hidden="true">2.7.</strong> CGO内存模型</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../ch2-cgo\ch2-08-class.html" ><strong aria-hidden="true">2.8.</strong> C++类包装</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../ch2-cgo\ch2-09-static-shared-lib.html" ><strong aria-hidden="true">2.9.</strong> 静态库和动态库</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../ch2-cgo\ch2-10-link.html" ><strong aria-hidden="true">2.10.</strong> 编译和链接参数</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../ch2-cgo\ch2-11-ext.html" ><strong aria-hidden="true">2.11.</strong> 补充说明</a>
    </li>
  </ol>
  <li class="chapter-item expanded ">
    <a href="../ch3-asm\readme.html" ><strong aria-hidden="true">3.</strong> 汇编语言</a>
  </li>
  <ol class="section">
    <li class="chapter-item expanded ">
      <a href="../ch3-asm\ch3-01-basic.html" ><strong aria-hidden="true">3.1.</strong> 快速入门</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../ch3-asm\ch3-02-arch.html" ><strong aria-hidden="true">3.2.</strong> 计算机结构</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../ch3-asm\ch3-03-const-and-var.html" ><strong aria-hidden="true">3.3.</strong> 常量和全局变量</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../ch3-asm\ch3-04-func.html" ><strong aria-hidden="true">3.4.</strong> 函数</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../ch3-asm\ch3-05-control-flow.html" ><strong aria-hidden="true">3.5.</strong> 控制流</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../ch3-asm\ch3-06-func-again.html" ><strong aria-hidden="true">3.6.</strong> 再论函数</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../ch3-asm\ch3-07-hack-asm.html" ><strong aria-hidden="true">3.7.</strong> 汇编语言的威力</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../ch3-asm\ch3-08-goroutine-id.html" ><strong aria-hidden="true">3.8.</strong> 例子：Goroutine ID</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../ch3-asm\ch3-09-debug.html" ><strong aria-hidden="true">3.9.</strong> Delve调试器</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../ch3-asm\ch3-10-ext.html" ><strong aria-hidden="true">3.10.</strong> 补充说明</a>
    </li>
  </ol>
  <li class="chapter-item expanded ">
    <a href="../ch4-rpc\readme.html" ><strong aria-hidden="true">4.</strong> 第4章 RPC和Protobuf</a>
  </li>
  <ol class="section">
    <li class="chapter-item expanded ">
      <a href="../ch4-rpc\ch4-01-rpc-intro.html" ><strong aria-hidden="true">4.1.</strong> RPC入门</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../ch4-rpc\ch4-02-pb-intro.html" ><strong aria-hidden="true">4.2.</strong> Protobuf</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../ch4-rpc\ch4-03-netrpc-hack.html" ><strong aria-hidden="true">4.3.</strong> 玩转RPC</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../ch4-rpc\ch4-04-grpc.html" ><strong aria-hidden="true">4.4.</strong> gRPC入门</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../ch4-rpc\ch4-05-grpc-hack.html" ><strong aria-hidden="true">4.5.</strong> gRPC进阶</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../ch4-rpc\ch4-06-grpc-ext.html" ><strong aria-hidden="true">4.6.</strong> gRPC和Protobuf扩展</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../ch4-rpc\ch4-07-pbgo.html" ><strong aria-hidden="true">4.7.</strong> pbgo: 基于Protobuf的框架</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../ch4-rpc\ch4-08-grpcurl.html" ><strong aria-hidden="true">4.8.</strong> grpcurl工具</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../ch4-rpc\ch4-09-ext.html" ><strong aria-hidden="true">4.9.</strong> 补充说明</a>
    </li>
  </ol>
  <li class="chapter-item expanded ">
    <a href="../ch5-web\readme.html" ><strong aria-hidden="true">5.</strong> Go和Web</a>
  </li>
  <ol class="section">
    <li class="chapter-item expanded ">
      <a href="../ch5-web\ch5-01-introduction.html" ><strong aria-hidden="true">5.1.</strong> Web开发简介</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../ch5-web\ch5-02-router.html" ><strong aria-hidden="true">5.2.</strong> 请求路由</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../ch5-web\ch5-03-middleware.html" ><strong aria-hidden="true">5.3.</strong> 中间件</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../ch5-web\ch5-04-validator.html" ><strong aria-hidden="true">5.4.</strong> 请求校验</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../ch5-web\ch5-05-database.html" class="active"><strong aria-hidden="true">5.5.</strong> 和数据库打交道</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../ch5-web\ch5-06-ratelimit.html" ><strong aria-hidden="true">5.6.</strong> 服务流量限制</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../ch5-web\ch5-07-layout-of-web-project.html" ><strong aria-hidden="true">5.7.</strong> 大型Web项目分层</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../ch5-web\ch5-08-interface-and-web.html" ><strong aria-hidden="true">5.8.</strong> 接口和表驱动开发</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../ch5-web\ch5-09-gated-launch.html" ><strong aria-hidden="true">5.9.</strong> 灰度发布和A/B测试</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../ch5-web\ch5-10-ext.html" ><strong aria-hidden="true">5.10.</strong> 补充说明</a>
    </li>
  </ol>
  <li class="chapter-item expanded ">
    <a href="../ch6-cloud\readme.html" ><strong aria-hidden="true">6.</strong> 分布式系统</a>
  </li>
  <ol class="section">
    <li class="chapter-item expanded ">
      <a href="../ch6-cloud\ch6-01-dist-id.html" ><strong aria-hidden="true">6.1.</strong> 分布式 id 生成器</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../ch6-cloud\ch6-02-lock.html" ><strong aria-hidden="true">6.2.</strong> 分布式锁</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../ch6-cloud\ch6-03-delay-job.html" ><strong aria-hidden="true">6.3.</strong> 延时任务系统</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../ch6-cloud\ch6-04-search-engine.html" ><strong aria-hidden="true">6.4.</strong> 分布式搜索引擎</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../ch6-cloud\ch6-05-load-balance.html" ><strong aria-hidden="true">6.5.</strong> 负载均衡</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../ch6-cloud\ch6-06-config.html" ><strong aria-hidden="true">6.6.</strong> 分布式配置管理</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../ch6-cloud\ch6-07-crawler.html" ><strong aria-hidden="true">6.7.</strong> 分布式爬虫</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../ch6-cloud\ch6-08-ext.html" ><strong aria-hidden="true">6.8.</strong> 补充说明</a>
    </li>
  </ol>
  <li class="chapter-item expanded ">
    <a href="../appendix\readme.html" ><strong aria-hidden="true">7.</strong> 附录</a>
  </li>
  <ol class="section">
    <li class="chapter-item expanded ">
      <a href="../appendix\appendix-a-trap.html" ><strong aria-hidden="true">7.1.</strong> 附录A: Go语言常见坑</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../appendix\appendix-b-gems.html" ><strong aria-hidden="true">7.2.</strong> 附录B: 有趣的代码片段</a>
    </li>
    <li class="chapter-item expanded ">
      <a href="../appendix\appendix-c-author.html" ><strong aria-hidden="true">7.3.</strong> 附录C: 作者简介</a>
    </li>
  </ol>
</ol>

            </div>
            <div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
        </nav>

        <div id="page-wrapper" class="page-wrapper">

            <div class="page">
                <div id="menu-bar-hover-placeholder"></div>
                <div id="menu-bar" class="menu-bar sticky bordered">
                    <div class="left-buttons">
                        <button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
                            <i class="fa fa-bars"></i>
                        </button>
                        <button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
                            <i class="fa fa-paint-brush"></i>
                        </button>
                        <ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
                            <li role="none"><button role="menuitem" class="theme" id="light">Light (default)</button></li>
                            <li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
                            <li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
                            <li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
                        </ul>
                    </div>

                    <h1 class="menu-title"><a href="../index.html">Go语言高级编程</a></h1>

                    <div class="right-buttons">
                        <a href="https://github.com/chai2010/advanced-go-programming-book" title="Git repository" aria-label="Git repository">
                            <i id="git-repository-button" class="fa fa-github"></i>
                        </a>
                        <a href="https://github.com/chai2010/advanced-go-programming-book/edit/master/ch5-web\ch5-05-database.md" title="Suggest an edit" aria-label="Suggest an edit">
                            <i id="git-edit-button" class="fa fa-edit"></i>
                        </a>
                    </div>
                </div>

                <!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
                <script type="text/javascript">
                    document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
                    document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
                    Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
                        link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
                    });
                </script>

                <div id="content" class="content">
                    <!-- Page table of contents -->
                    <div class="sidetoc"><nav class="pagetoc"></nav></div>

                    <main>
                        <ul dir="auto"><li><em>凹语言(Go实现, 面向WASM设计): <a href="https://github.com/wa-lang/wa">https://github.com/wa-lang/wa</a></em></li><li><em>WaBook(Go语言实现的MD电子书构建工具): <a href="https://github.com/wa-lang/wabook">https://github.com/wa-lang/wabook</a></em></li></ul><hr>

                        <h1>5.5 Database 和数据库打交道</h1>
<p>本节将对 <code>db/sql</code> 官方标准库作一些简单分析，并介绍一些应用比较广泛的开源 ORM 和 SQL Builder。并从企业级应用开发和公司架构的角度来分析哪种技术栈对于现代的企业级应用更为合适。</p>
<h2>5.5.1 从 database/sql 讲起</h2>
<p>Go 官方提供了 <code>database/sql</code> 包来给用户进行和数据库打交道的工作，<code>database/sql</code> 库实际只提供了一套操作数据库的接口和规范，例如抽象好的 SQL 预处理（prepare），连接池管理，数据绑定，事务，错误处理等等。官方并没有提供具体某种数据库实现的协议支持。</p>
<p>和具体的数据库，例如 MySQL 打交道，还需要再引入 MySQL 的驱动，像下面这样：</p>
<pre><code class="language-go">import &quot;database/sql&quot;
import _ &quot;github.com/go-sql-driver/mysql&quot;

db, err := sql.Open(&quot;mysql&quot;, &quot;user:password@/dbname&quot;)
</code></pre>
<pre><code class="language-go">import _ &quot;github.com/go-sql-driver/mysql&quot;
</code></pre>
<p>这条 import 语句会调用了 <code>mysql</code> 包的 <code>init</code> 函数，做的事情也很简单：</p>
<pre><code class="language-go">func init() {
	sql.Register(&quot;mysql&quot;, &amp;MySQLDriver{})
}
</code></pre>
<p>在 <code>sql</code> 包的全局 <code>map</code> 里把 <code>mysql</code> 这个名字的 <code>driver</code> 注册上。<code>Driver</code> 在 <code>sql</code> 包中是一个接口：</p>
<pre><code class="language-go">type Driver interface {
	Open(name string) (Conn, error)
}
</code></pre>
<p>调用 <code>sql.Open()</code> 返回的 <code>db</code> 对象就是这里的 <code>Conn</code>。</p>
<pre><code class="language-go">type Conn interface {
	Prepare(query string) (Stmt, error)
	Close() error
	Begin() (Tx, error)
}
</code></pre>
<p>也是一个接口。如果你仔细地查看 <code>database/sql/driver/driver.go</code> 的代码会发现，这个文件里所有的成员全都是接口，对这些类型进行操作，还是会调用具体的 <code>driver</code> 里的方法。</p>
<p>从用户的角度来讲，在使用 <code>database/sql</code> 包的过程中，你能够使用的也就是这些接口里提供的函数。来看一个使用 <code>database/sql</code> 和 <code>go-sql-driver/mysql</code> 的完整的例子：</p>
<pre><code class="language-go">package main

import (
	&quot;database/sql&quot;
	_ &quot;github.com/go-sql-driver/mysql&quot;
)

func main() {
	// db 是一个 sql.DB 类型的对象
	// 该对象线程安全，且内部已包含了一个连接池
	// 连接池的选项可以在 sql.DB 的方法中设置，这里为了简单省略了
	db, err := sql.Open(&quot;mysql&quot;,
		&quot;user:password@tcp(127.0.0.1:3306)/hello&quot;)
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	var (
		id int
		name string
	)
	rows, err := db.Query(&quot;select id, name from users where id = ?&quot;, 1)
	if err != nil {
		log.Fatal(err)
	}

	defer rows.Close()

	// 必须要把 rows 里的内容读完，或者显式调用 Close() 方法，
	// 否则在 defer 的 rows.Close() 执行之前，连接永远不会释放
	for rows.Next() {
		err := rows.Scan(&amp;id, &amp;name)
		if err != nil {
			log.Fatal(err)
		}
		log.Println(id, name)
	}

	err = rows.Err()
	if err != nil {
		log.Fatal(err)
	}
}
</code></pre>
<p>如果读者想了解官方这个 <code>database/sql</code> 库更加详细的用法的话，可以参考 <code>http://go-database-sql.org/</code>。</p>
<p>包括该库的功能介绍、用法、注意事项和反直觉的一些实现方式（例如同一个 goroutine 内对 <code>sql.DB</code> 的查询，可能在多个连接上）都有涉及，本章中不再赘述。</p>
<p>聪明如你的话，在上面这段简短的程序中可能已经嗅出了一些不好的味道。官方的 <code>db</code> 库提供的功能这么简单，我们每次去数据库里读取内容岂不是都要去写这么一套差不多的代码？或者如果我们的对象是结构体，把 <code>sql.Rows</code> 绑定到对象的工作就会变得更加得重复而无聊。</p>
<p>是的，所以社区才会有各种各样的 SQL Builder 和 ORM 百花齐放。</p>
<h2>5.5.2 提高生产效率的 ORM 和 SQL Builder</h2>
<p>在 Web 开发领域常常提到的 ORM 是什么？我们先看看万能的维基百科：</p>
<blockquote>
<p>对象关系映射（英语：Object Relational Mapping，简称 ORM，或 O/RM，或 O/R mapping），是一种程序设计技术，用于实现面向对象编程语言里不同类型系统的数据之间的转换。
从效果上说，它其实是创建了一个可在编程语言里使用的 “虚拟对象数据库”。</p>
</blockquote>
<p>最为常见的 ORM 做的是从 db 到程序的类或结构体这样的映射。所以你手边的程序可能是从 MySQL 的表映射你的程序内的类。我们可以先来看看其它的程序语言里的 ORM 写起来是怎么样的感觉：</p>
<pre><code class="language-python">&gt;&gt;&gt; from blog.models import Blog
&gt;&gt;&gt; b = Blog(name='Beatles Blog', tagline='All the latest Beatles news.')
&gt;&gt;&gt; b.save()
</code></pre>
<p>完全没有数据库的痕迹，没错，ORM 的目的就是屏蔽掉 DB 层，很多语言的 ORM 只要把你的类或结构体定义好，再用特定的语法将结构体之间的一对一或者一对多关系表达出来。那么任务就完成了。然后你就可以对这些映射好了数据库表的对象进行各种操作，例如 save、create、retrieve、delete。至于 ORM 在背地里做了什么阴险的勾当，你是不一定清楚的。使用 ORM 的时候，我们往往比较容易有一种忘记了数据库的直观感受。举个例子，我们有个需求：向用户展示最新的商品列表，我们再假设，商品和商家是 1:1 的关联关系，我们就很容易写出像下面这样的代码：</p>
<pre><code class="language-python"># 伪代码
shopList := []
for product in productList {
	shopList = append(shopList, product.GetShop)
}
</code></pre>
<p>当然了，我们不能批判这样写代码的程序员是偷懒的程序员。因为 ORM 一类的工具在出发点上就是屏蔽 sql，让我们对数据库的操作更接近于人类的思维方式。这样很多只接触过 ORM 而且又是刚入行的程序员就很容易写出上面这样的代码。</p>
<p>这样的代码将对数据库的读请求放大了 N 倍。也就是说，如果你的商品列表有 15 个 SKU，那么每次用户打开这个页面，至少需要执行 1（查询商品列表）+ 15（查询相关的商铺信息）次查询。这里 N 是 16。如果你的列表页很大，比如说有 600 个条目，那么你就至少要执行 1+600 次查询。如果说你的数据库能够承受的最大的简单查询是 12 万 QPS，而上述这样的查询正好是你最常用的查询的话，你能对外提供的服务能力是多少呢？是 200 qps！互联网系统的忌讳之一，就是这种无端的读放大。</p>
<p>当然，你也可以说这不是 ORM 的问题，如果你手写 sql 你还是可能会写出差不多的程序，那么再来看两个 demo：</p>
<pre><code class="language-go">o := orm.NewOrm()
num, err := o.QueryTable(&quot;cardgroup&quot;).Filter(&quot;Cards__Card__Name&quot;, cardName).All(&amp;cardgroups)
</code></pre>
<p>很多 ORM 都提供了这种 Filter 类型的查询方式，不过在某些 ORM 背后可能隐藏了非常难以察觉的细节，比如生成的 SQL 语句会自动 <code>limit 1000</code>。</p>
<p>也许喜欢 ORM 的读者读到这里会反驳了，你是没有认真阅读文档就瞎写。是的，尽管这些 ORM 工具在文档里说明了 All 查询在不显式地指定 Limit 的话会自动 limit 1000，但对于很多没有阅读过文档或者看过 ORM 源码的人，这依然是一个非常难以察觉的 “魔鬼” 细节。喜欢强类型语言的人一般都不喜欢语言隐式地去做什么事情，例如各种语言在赋值操作时进行的隐式类型转换然后又在转换中丢失了精度的勾当，一定让你非常的头疼。所以一个程序库背地里做的事情还是越少越好，如果一定要做，那也一定要在显眼的地方做。比如上面的例子，去掉这种默认的自作聪明的行为，或者要求用户强制传入 limit 参数都是更好的选择。</p>
<p>除了 limit 的问题，我们再看一遍这个下面的查询：</p>
<pre><code class="language-go">num, err := o.QueryTable(&quot;cardgroup&quot;).Filter(&quot;Cards__Card__Name&quot;, cardName).All(&amp;cardgroups)
</code></pre>
<p>你可以看得出来这个 Filter 是有表 join 的操作么？当然了，有深入使用经验的用户还是会觉得这是在吹毛求疵。但这样的分析想证明的是，ORM 想从设计上隐去太多的细节。而方便的代价是其背后的运行完全失控。这样的项目在经过几任维护人员之后，将变得面目全非，难以维护。</p>
<p>当然，我们不能否认 ORM 的进步意义，它的设计初衷就是为了让数据的操作和存储的具体实现相剥离。但是在上了规模的公司的人们渐渐达成了一个共识，由于隐藏重要的细节，ORM 可能是失败的设计。其所隐藏的重要细节对于上了规模的系统开发来说至关重要。</p>
<p>相比 ORM 来说，SQL Builder 在 SQL 和项目可维护性之间取得了比较好的平衡。首先 sql builder 不像 ORM 那样屏蔽了过多的细节，其次从开发的角度来讲，SQL Builder 进行简单封装后也可以非常高效地完成开发，举个例子：</p>
<pre><code class="language-go">where := map[string]interface{} {
	&quot;order_id &gt; ?&quot; : 0,
	&quot;customer_id != ?&quot; : 0,
}
limit := []int{0,100}
orderBy := []string{&quot;id asc&quot;, &quot;create_time desc&quot;}

orders := orderModel.GetList(where, limit, orderBy)
</code></pre>
<p>写 SQL Builder 的相关代码，或者读懂都不费劲。把这些代码脑内转换为 sql 也不会太费劲。所以通过代码就可以对这个查询是否命中数据库索引，是否走了覆盖索引，是否能够用上联合索引进行分析了。</p>
<p>说白了 SQL Builder 是 sql 在代码里的一种特殊方言，如果你们没有 DBA 但研发有自己分析和优化 sql 的能力，或者你们公司的 DBA 对于学习这样一些 sql 的方言没有异议。那么使用 SQL Builder 是一个比较好的选择，不会导致什么问题。</p>
<p>另外在一些本来也不需要 DBA 介入的场景内，使用 SQL Builder 也是可以的，例如你要做一套运维系统，且将 MySQL 当作了系统中的一个组件，系统的 QPS 不高，查询不复杂等等。</p>
<p>一旦你做的是高并发的 OLTP 在线系统，且想在人员充足分工明确的前提下最大程度控制系统的风险，使用 SQL Builder 就不合适了。</p>
<h2>5.5.3 脆弱的数据库</h2>
<p>无论是 ORM 还是 SQL Builder 都有一个致命的缺点，就是没有办法进行系统上线的事前 sql 审核。虽然很多 ORM 和 SQL Builder 也提供了运行期打印 sql 的功能，但只在查询的时候才能进行输出。而 SQL Builder 和 ORM 本身提供的功能太过灵活。使得你不可能通过测试枚举出所有可能在线上执行的 sql。例如你可能用 SQL Builder 写出下面这样的代码：</p>
<pre><code class="language-go">where := map[string]interface{} {
	&quot;product_id = ?&quot; : 10,
	&quot;user_id = ?&quot; : 1232 ,
}

if order_id != 0 {
	where[&quot;order_id = ?&quot;] = order_id
}

res, err := historyModel.GetList(where, limit, orderBy)
</code></pre>
<p>你的系统里有大量类似上述样例的 <code>if</code> 的话，就难以通过测试用例来覆盖到所有可能的 sql 组合了。</p>
<p>这样的系统只要发布，就已经孕育了初期的巨大风险。</p>
<p>对于现在 7 乘 24 服务的互联网公司来说，服务不可用是非常重大的问题。存储层的技术栈虽经历了多年的发展，在整个系统中依然是最为脆弱的一环。系统宕机对于 24 小时对外提供服务的公司来说，意味着直接的经济损失。各种风险不可忽视。</p>
<p>从行业分工的角度来讲，现今的互联网公司都有专职的 DBA。大多数 DBA 并不一定有写代码的能力，去阅读 SQL Builder 的相关 “拼 SQL” 代码多多少少还是会有一点障碍。从 DBA 角度出发，还是希望能够有专门的事前 SQL 审核机制，并能让其低成本地获取到系统的所有 SQL 内容，而不是去阅读业务研发编写的 SQL Builder 的相关代码。</p>
<p>所以现如今，大型的互联网公司核心线上业务都会在代码中把 SQL 放在显眼的位置提供给 DBA 评审，举一个例子：</p>
<pre><code class="language-go">const (
	getAllByProductIDAndCustomerID = `select * from p_orders where product_id in (:product_id) and customer_id=:customer_id`
)

// GetAllByProductIDAndCustomerID
// @param driver_id
// @param rate_date
// @return []Order, error
func GetAllByProductIDAndCustomerID(ctx context.Context, productIDs []uint64, customerID uint64) ([]Order, error) {
	var orderList []Order

	params := map[string]interface{}{
		&quot;product_id&quot; : productIDs,
		&quot;customer_id&quot;: customerID,
	}

	// getAllByProductIDAndCustomerID 是 const 类型的 sql 字符串
	sql, args, err := sqlutil.Named(getAllByProductIDAndCustomerID, params)
	if err != nil {
		return nil, err
	}

	err = dao.QueryList(ctx, sqldbInstance, sql, args, &amp;orderList)
	if err != nil {
		return nil, err
	}

	return orderList, err
}
</code></pre>
<p>像这样的代码，在上线之前把 DAO 层的变更集的 const 部分直接拿给 DBA 来进行审核，就比较方便了。代码中的 <code>sqlutil.Named</code> 是类似于 sqlx 中的 <code>Named</code> 函数，同时支持 where 表达式中的比较操作符和 in。</p>
<p>这里为了说明简便，函数写得稍微复杂一些，仔细思考一下的话查询的导出函数还可以进一步进行简化。请读者朋友们自行尝试。</p>


                        <hr><table><tr><td><img width="222px" src="https://chai2010.cn/advanced-go-programming-book/css.png"></td><td><img width="222px" src="https://chai2010.cn/advanced-go-programming-book/cch.png"></td></tr></table>

                        
                            <div id="giscus-container"></div>
                        

                        
                            <footer class="page-footer">
                                <span>© 2019-2022 | <a href="https://github.com/chai2010/advanced-go-programming-book">柴树杉、曹春晖</a> 保留所有权利</span>
                            </footer>
                        
                    </main>

                    <nav class="nav-wrapper" aria-label="Page navigation">
                        <!-- Mobile navigation buttons -->
                        
                            <a rel="prev" href="../ch5-web\ch5-04-validator.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
                                <i class="fa fa-angle-left"></i>
                            </a>
                        
                        
                            <!-- ../ch5-web\ch5-06-ratelimit.html -->
                            <a rel="next" href="../ch5-web\ch5-06-ratelimit.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
                                <i class="fa fa-angle-right"></i>
                            </a>
                        
                        <div style="clear: both"></div>
                    </nav>
                </div>
            </div>

            <nav class="nav-wide-wrapper" aria-label="Page navigation">
                
                    <a rel="prev" href="../ch5-web\ch5-04-validator.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
                        <i class="fa fa-angle-left"></i>
                    </a>
                
                
                    <a rel="next" href="../ch5-web\ch5-06-ratelimit.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
                        <i class="fa fa-angle-right"></i>
                    </a>
                
            </nav>

        </div>

        <script type="text/javascript">
            window.playground_copyable = true;
        </script>
        <script src="../static/wabook/mark.min.js" type="text/javascript" charset="utf-8"></script>
        <script src="../static/wabook/clipboard.min.js" type="text/javascript" charset="utf-8"></script>
        <script src="../static/wabook/highlight.js" type="text/javascript" charset="utf-8"></script>
        <script src="../static/wabook/book.js" type="text/javascript" charset="utf-8"></script>
        
        <script type="text/javascript" charset="utf-8">
            var pagePath = "ch5-web\ch5-05-database.md"
        </script>

        <!-- Custom JS scripts -->
        
            <script src="../static/wabook/giscus.js" type="text/javascript" charset="utf-8"></script>
        

    </body>
</html>
